import java.util.*;
import java.net.*;
import java.nio.*;
import java.io.*;
import java.security.*;
import java.applet.*;
import java.util.*;
import java.lang.reflect.*;
import org.json.simple.*;
import org.json.simple.parser.*;
import netscape.javascript.*;



public class Client extends Applet
{

  public static final long serialVersionUID = 1L;
  private HashMap<String, ClientData> client_data = null;
  private JSONParser jsonP = null;

  private class ClientData
  {
    public Listener listener;
    public Thread thread;
    public ClientData(Thread thread, Listener listener)
    {
      this.thread = thread;
      this.listener = listener;
    }
  }

  private ClientData getClientDataById(String conid) throws Exception
  {
    if (conid == null)
      throw new Exception("conid argument required");


    for(Map.Entry<String, ClientData> entry : client_data.entrySet())
      if (entry.getKey().equals(conid))
        return entry.getValue();
     
    return null;
  }


  private String stringJSONException(Exception e)
  {
    return stringJSONException(e, null);
  }

  private String stringJSONException(Exception e, Integer sequence)
  {
    HashMap<String, Object> obj = new HashMap<String, Object>();


    obj.put("exception", e.getClass().getName());
    obj.put("message", e.getMessage());

    if (e.getCause() != null)
      obj.put("cause", e.getCause().toString());
   
    if (sequence != null)
      obj.put("sequence", sequence.toString());
   
    StackTraceElement stack[] = e.getStackTrace();
    List<String> jsonStack = new Vector<String>();
    for (int i=0; i < stack.length; i++)
      jsonStack.add(stack[i].toString());

    obj.put("stacktrace",  jsonStack);

    return JSONValue.toJSONString(obj);
  }

  private String stringJSONResult(Object result)
  {
    HashMap<String, Object> obj = new HashMap<String, Object>();
    obj.put("exception", false);
    obj.put("data", result);

    return JSONValue.toJSONString(obj);
  }


  public Client()
  {
    client_data = new HashMap<String, ClientData>();
    jsonP = new JSONParser();
  }

  private class Listener implements Runnable
  {
    public Connection conn = null;
    public String receive_callback = null;

    String uid;
    public int port;
    public String host;
    public String pass;
    public String user;
  
    public Listener(String host, int port, String user, String pass, String receive_callback)
    {
      this.port = port;
      this.host = host;
      this.user = user;
      this.pass = pass;
      this.receive_callback = receive_callback;
      uid = UUID.randomUUID().toString();
    }

    public String getId()
    {
      return uid;
    }

    public void run()
    {
      conn = new Connection();

      System.out.println("== Starting Async Connection ==");
      try
      {
        if (!conn.connect(host, port))
           throw new Exception("cannot connect to "+host+":"+port);

        if (!conn.authenticate(user, pass))
          throw new Exception("invalid username or password");
      }
      catch(Exception e)
      {
        /* Sequence 0 - to identify connection exceptions */
        invokeJS(this.receive_callback, stringJSONException(e, 0));
        return;
      }

      System.out.println("== Connection Succeeded, Going Into Receive Loop ==");

      alert("Connection Succeeded!");

      /* send connection id response */
      List<String> words = new Vector<String>();
      words.add("0"); 
      words.add("ConnectionID");
      words.add(getId());
      invokeJS(this.receive_callback, stringJSONResult(words));

      while(true)
      {
        try
        {
         words = conn.recv();
         invokeJS(this.receive_callback, stringJSONResult(words));
        }
       catch(Exception e)
        {
          invokeJS(this.receive_callback, stringJSONException(e));
        }
      }
    }
  }
 
  public String connect(String arg) 
  {
     try
     {

       Object arg_obj = jsonP.parse(arg);

       if (!(arg_obj instanceof HashMap))
         throw new Exception("invalid argument"); 

       @SuppressWarnings("unchecked")
       HashMap<String, Object> obj = (HashMap<String, Object>) arg_obj;

       /* validate the required arguments */
       if (!obj.containsKey("host") || !(obj.get("host") instanceof String))
         throw new Exception("host argument required");

       if (!obj.containsKey("port") || !(obj.get("port") instanceof Number) )
         throw new Exception("port argument required");

       if (!obj.containsKey("user") || !(obj.get("user") instanceof String))
         throw new Exception("user argument required");
 
       if (!obj.containsKey("pass") || !(obj.get("pass") instanceof String))
         throw new Exception("pass argument required");
 
       if (!obj.containsKey("cb") || !(obj.get("cb") instanceof String))
         throw new Exception("cb argument required");


       String host = (String) obj.get("host");
       String user = (String) obj.get("user");
       String pass = (String) obj.get("pass");
       String cb   = (String) obj.get("cb");
       int port = ((Number) obj.get("port")).intValue();

       /* Create the listener */
       Listener cl = new Listener(host, port, user, pass, cb);    
       String id = cl.getId();

       /* Create a thread for the listner */
       Thread cl_thread = new Thread(cl);
 

       // save the thread, and listener information
       client_data.put(id, new ClientData(cl_thread, cl));

       /* start the thread */
       cl_thread.start();

       /* return the listner identifier */
       return stringJSONResult(id);
     }
    catch(Exception e)
     {
       return stringJSONException(e);
     }
  }

  public String send(String arg)
  {
    try
    {
 
      Object arg_obj = jsonP.parse(arg);

      if (!(arg_obj instanceof HashMap))
        throw new Exception("invalid argument");

      @SuppressWarnings("unchecked")
      HashMap<String, Object> obj = (HashMap<String, Object>) arg_obj;

      if (!obj.containsKey("conid") || !(obj.get("conid") instanceof String))
        throw new Exception("conid argument required");

      if (!obj.containsKey("words") || !(obj.get("words") instanceof List))
        throw new Exception("words argument required");

      String conid = (String) obj.get("conid");
      List<Object> words_obj = (List<Object>) obj.get("words");
     
      // validate that the words are strings
      for(int i=0; i < words_obj.size(); i++)
        if (!(words_obj.get(i) instanceof String))
          throw new Exception("words["+i+"] is not a string");
       
      List<String> words = (List<String>)(List<?>) words_obj;

      /*
      alert("data: conid: "+conid);
      for(int i=0; i < words.size(); i++)
        alert("data: words["+i+"]: "+words.get(i));
      */

      ClientData cdata = getClientDataById(conid);
      if (cdata == null)
        throw new Exception("no client data associated with id "+conid);

      int sequence = cdata.listener.conn.send(words); 

      return stringJSONResult(sequence);
    }
    catch(Exception e)
    {
      return stringJSONException(e);
    }

    

  }

  public Boolean evalJavaScript(String str)
  {
    try
    {
      JSObject win = JSObject.getWindow(this);
      win.eval(str);
      return true;
    }
    catch(Exception e)
    {
      return false;
    }
  }

  public void init()
  {
  }

  public void alert(String str)
  {
    /* safety replacements */
    str = str.replace("\\", "\\\\");
    str = str.replaceAll("\"", "\\\\\\\"");

    evalJavaScript("alert(\""+str+"\");");
  }

  public void invokeJS(String function, String arg)
  {
    /* safety replacements */
    arg = arg.replace("\\", "\\\\");
    arg = arg.replaceAll("\"", "\\\\\\\"");
    evalJavaScript(function+"(\""+arg+"\");");
  }

}

